/*
 * LipeRMI - a light weight Internet approach for remote method invocation
 * Copyright (C) 2006  Felipe Santos Andrade
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * For more information, see http://lipermi.sourceforge.net/license.php
 * You can also contact author through lipeandrade@users.sourceforge.net
 */

package handler;

import handler.filter.IProtocolFilter;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import call.IRemoteMessage;
import call.RemoteCall;
import call.RemoteInstance;
import call.RemoteReturn;
import exception.LipeRMIException;

/**
 * A ConnectionHandler is object which can call remote methods, receive remote
 * calls and dispatch its returns.
 * 
 * @author lipe
 * @date 05/10/2006
 * 
 * @see lipermi.handler.CallHandler
 * @see lipermi.handler.RemoteInstance
 * @see lipermi.handler.RemoteCall
 * @see lipermi.handler.RemoteReturn
 * @see lipermi.net.Client
 * @see AppServer.net.Server
 * @see lipermi.filter.DefaultFilter
 */
public class ConnectionHandler implements Runnable {

	public static ConnectionHandler createConnectionHandler(Socket socket,
			CallHandler callHandler, IProtocolFilter filter) {
		ConnectionHandler connectionHandler = new ConnectionHandler(socket,
				callHandler, filter);

		String threadName = String
				.format("ConnectionHandler (%s:%d)", socket.getInetAddress().getHostAddress(), socket.getPort()); //$NON-NLS-1$
		Thread connectionHandlerThread = new Thread(connectionHandler,
				threadName);
		connectionHandlerThread.setDaemon(true);
		connectionHandlerThread.start();

		return connectionHandler;
	}

	public static ConnectionHandler createConnectionHandler(Socket socket,
			CallHandler callHandler, IProtocolFilter filter,
			IConnectionHandlerListener listener) {
		ConnectionHandler connectionHandler = createConnectionHandler(socket,
				callHandler, filter);
		connectionHandler.addConnectionHandlerListener(listener);
		return connectionHandler;
	}

	private CallHandler callHandler;

	private Socket socket;

	private ObjectOutputStream output;

	private Long callId = 0L;

	private IProtocolFilter filter;

	private List<IConnectionHandlerListener> listeners = new LinkedList<IConnectionHandlerListener>();

	private Map<RemoteInstance, Object> remoteInstanceProxys = new HashMap<RemoteInstance, Object>();

	private List<RemoteReturn> remoteReturns = new LinkedList<RemoteReturn>();

	public void addConnectionHandlerListener(IConnectionHandlerListener listener) {
		listeners.add(listener);
	}

	public void removeConnectionHandlerListener(
			IConnectionHandlerListener listener) {
		listeners.remove(listener);
	}

	private ConnectionHandler(Socket socket, CallHandler callHandler,
			IProtocolFilter filter) {
		this.callHandler = callHandler;
		this.socket = socket;
		this.filter = filter;
	}

	@Override
	public void run() {
		ObjectInputStream input;

		try {
			input = new ObjectInputStream(socket.getInputStream());

			while (socket.isConnected()) {
				Object objFromStream = input.readObject();

				IRemoteMessage remoteMessage = filter.readObject(objFromStream);

				if (remoteMessage instanceof RemoteCall) {

					final RemoteCall remoteCall = (RemoteCall) remoteMessage;
					if (remoteCall.getArgs() != null) {
						for (int n = 0; n < remoteCall.getArgs().length; n++) {
							Object arg = remoteCall.getArgs()[n];
							if (arg instanceof RemoteInstance) {
								RemoteInstance remoteInstance = (RemoteInstance) arg;
								remoteCall.getArgs()[n] = getProxyFromRemoteInstance(remoteInstance);
							}
						}
					}

					Thread delegator = new Thread(new Runnable() {
						@Override
						public void run() {
							CallLookup.handlingMe(ConnectionHandler.this);

							RemoteReturn remoteReturn;
							try {
								remoteReturn = callHandler.delegateCall(remoteCall);
								sendMessage(remoteReturn);
							} catch (Exception e) {
								e.printStackTrace();
							}

							CallLookup.forgetMe();
						}
					}, "Delegator"); //$NON-NLS-1$
					delegator.setDaemon(true);
					delegator.start();
				} else if (remoteMessage instanceof RemoteReturn) {
					RemoteReturn remoteReturn = (RemoteReturn) remoteMessage;
					synchronized (remoteReturns) {
						remoteReturns.add(remoteReturn);
						remoteReturns.notifyAll();
					}
				} else
					throw new LipeRMIException("Unknown IRemoteMessage type"); //$NON-NLS-1$
			}
		} catch (Exception e) {
			try {
				socket.close();
			} catch (IOException e1) {
			}

			synchronized (remoteReturns) {
				remoteReturns.notifyAll();
			}

			for (IConnectionHandlerListener listener : listeners)
				listener.connectionClosed();
		}
	}

	private synchronized void sendMessage(IRemoteMessage remoteMessage)
			throws IOException {
		if (output == null)
			output = new ObjectOutputStream(socket.getOutputStream());

		Object objToWrite = filter.prepareWrite(remoteMessage);

		output.writeObject(objToWrite);
		output.flush();
	}

	@SuppressWarnings("rawtypes")
	final Object remoteInvocation(final Object proxy, final Method method,
			final Object[] args) throws Throwable {
		final Long id = callId++;

		RemoteInstance remoteInstance;
		remoteInstance = getRemoteInstanceFromProxy(proxy);
		if (remoteInstance == null)
			remoteInstance = new RemoteInstance(null, proxy.getClass()
					.getInterfaces()[0].getName());

		if (args != null) {
			for (int n = 0; n < args.length; n++) {
				RemoteInstance remoteRef = callHandler
						.getRemoteReference(args[n]);
				if (remoteRef != null)
					args[n] = remoteRef;
			}
		}

		String methodId = method.toString().substring(15);

		IRemoteMessage remoteCall = new RemoteCall(remoteInstance, methodId,
				args, id);
		sendMessage(remoteCall);

		RemoteReturn remoteReturn = null;

		boolean bReturned = false;
		do {
			synchronized (remoteReturns) {
				for (RemoteReturn ret : remoteReturns) {
					if (ret.getCallId().equals(id)) {
						bReturned = true;
						remoteReturn = ret;
						break;
					}
				}
				if (bReturned)
					remoteReturns.remove(remoteReturn);
				else {
					try {
						remoteReturns.wait();
					} catch (InterruptedException ie) {
					}
				}
			}
		} while (socket.isConnected() && !bReturned);

		if (!socket.isConnected() && !bReturned)
			throw new LipeRMIException("Connection aborted"); //$NON-NLS-1$

		if (remoteReturn.isThrowing()
				&& remoteReturn.getRet() instanceof Throwable)
			throw ((Throwable) remoteReturn.getRet()).getCause();

		if (remoteReturn.getRet() instanceof RemoteInstance) {
			RemoteInstance remoteInstanceReturn = (RemoteInstance) remoteReturn
					.getRet();
			Object proxyReturn = remoteInstanceProxys.get(remoteInstanceReturn);
			if (proxyReturn == null) {
				Class clazz = Class
						.forName(remoteInstanceReturn.getClassName());
				proxyReturn = Proxy.newProxyInstance(clazz.getClassLoader(),
						new Class[] { clazz }, new CallProxy(this));
				remoteInstanceProxys.put(remoteInstanceReturn, proxyReturn);
			}
			return proxyReturn;
		}

		return remoteReturn.getRet();
	}

	private Object getProxyFromRemoteInstance(RemoteInstance remoteInstance) {
		Object proxy = remoteInstanceProxys.get(remoteInstance);
		if (proxy == null) {
			try {
				proxy = CallProxy.buildProxy(remoteInstance, this);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
			remoteInstanceProxys.put(remoteInstance, proxy);
		}
		return proxy;
	}

	private RemoteInstance getRemoteInstanceFromProxy(Object proxy) {
		for (RemoteInstance remoteInstance : remoteInstanceProxys.keySet()) {
			if (remoteInstanceProxys.get(remoteInstance) == proxy)
				return remoteInstance;
		}

		return null;
	}

	public Socket getSocket() {
		return socket;
	}
}
